/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
 */
package org.olap4j.impl;

import java.util.*;

/**
 * Parser for olap4j connect strings.
 * 
 * @author jhyde
 * @version $Id$
 * @since Dec 12, 2007
 */
// Copied from mondrian.olap.Util.ConnectStringParser
public class ConnectStringParser {
	private final String s;
	private final int n;
	private int i;
	private final StringBuilder nameBuf;
	private final StringBuilder valueBuf;

	/**
	 * Converts an OLE DB connect string into a {@link java.util.Map}.
	 * 
	 * <p>
	 * For example, <code>"Provider=MSOLAP; DataSource=LOCALHOST;"</code> becomes the set of (key, value) pairs
	 * <code>{("Provider","MSOLAP"),
	 * ("DataSource", "LOCALHOST")}</code>. Another example is
	 * <code>Provider='sqloledb';Data Source='MySqlServer';Initial
	 * Catalog='Pubs';Integrated Security='SSPI';</code>.
	 * 
	 * <p>
	 * This method implements as much as possible of the <a
	 * href="http://msdn.microsoft.com/library/en-us/oledb/htm/oledbconnectionstringsyntax.asp" target="_blank">OLE DB
	 * connect string syntax specification</a>.
	 * 
	 * <p>
	 * The return value is a map which:
	 * <ul>
	 * <li>preserves the order in that the entries occurred;</li>
	 * <li>is not case-sensitive when looking up properties</li>
	 * </ul>
	 * 
	 * @param s
	 *            Connect string
	 * 
	 * @return Map containing (name, value) pairs, stored in the order that they occurred in the connect string
	 */
	public static Map<String, String> parseConnectString(String s) {
		return new ConnectStringParser(s).parse();
	}

	private ConnectStringParser(String s) {
		this.s = s;
		this.i = 0;
		this.n = s.length();
		this.nameBuf = new StringBuilder(64);
		this.valueBuf = new StringBuilder(64);
	}

	private PropertyMap parse() {
		PropertyMap map = new PropertyMap();
		while (i < n) {
			parsePair(map);
		}
		return map;
	}

	/**
	 * Reads "name=value;" or "name=value<EOF>".
	 * 
	 * @param map
	 *            Map to append value to
	 */
	private void parsePair(PropertyMap map) {
		String name = parseName();
		if (name == null) {
			return;
		}
		String value;
		if (i >= n) {
			value = "";
		} else if (s.charAt(i) == ';') {
			i++;
			value = "";
		} else {
			value = parseValue();
		}
		map.put(name, value);
	}

	/**
	 * Reads "name=". Name can contain equals sign if equals sign is doubled.
	 * 
	 * @return Next name in the connect string being parsed, or null if there is no further name
	 */
	private String parseName() {
		nameBuf.setLength(0);
		while (true) {
			char c = s.charAt(i);
			switch (c) {
			case '=':
				i++;
				if (i < n && (c = s.charAt(i)) == '=') {
					// doubled equals sign; take one of them, and carry on
					i++;
					nameBuf.append(c);
					break;
				}
				String name = nameBuf.toString();
				name = name.trim();
				return name;
			case ' ':
				if (nameBuf.length() == 0) {
					// ignore preceding spaces
					i++;
					if (i >= n) {
						// there is no name, e.g. trailing spaces after
						// semicolon, 'x=1; y=2; '
						return null;
					}
					break;
				} else {
					// fall through
				}
			default:
				nameBuf.append(c);
				i++;
				if (i >= n) {
					return nameBuf.toString().trim();
				}
			}
		}
	}

	/**
	 * Reads "value;" or "value<EOF>"
	 * 
	 * @return next value from connect string being parsed
	 */
	private String parseValue() {
		char c;
		// skip over leading white space
		while ((c = s.charAt(i)) == ' ') {
			i++;
			if (i >= n) {
				return "";
			}
		}
		if (c == '"' || c == '\'') {
			String value = parseQuoted(c);
			// skip over trailing white space
			while (i < n && (c = s.charAt(i)) == ' ') {
				i++;
			}
			if (i >= n) {
				return value;
			} else if (c == ';') {
				i++;
				return value;
			} else {
				throw new RuntimeException("quoted value ended too soon, at position " + i + " in '" + s + "'");
			}
		} else {
			String value;
			int semi = s.indexOf(';', i);
			if (semi >= 0) {
				value = s.substring(i, semi);
				i = semi + 1;
			} else {
				value = s.substring(i);
				i = n;
			}
			return value.trim();
		}
	}

	/**
	 * Reads a string quoted by a given character. Occurrences of the quoting character must be doubled. For example,
	 * <code>parseQuoted('"')</code> reads <code>"a ""new"" string"</code> and returns <code>a "new" string</code>.
	 * 
	 * @param q
	 *            Quoting character (usually single or double quote)
	 * @return quoted string
	 */
	private String parseQuoted(char q) {
		char c = s.charAt(i++);
		assert c == q;
		valueBuf.setLength(0);
		while (i < n) {
			c = s.charAt(i);
			if (c == q) {
				i++;
				if (i < n) {
					c = s.charAt(i);
					if (c == q) {
						valueBuf.append(c);
						i++;
						continue;
					}
				}
				return valueBuf.toString();
			} else {
				valueBuf.append(c);
				i++;
			}
		}
		throw new RuntimeException("Connect string '" + s + "' contains unterminated quoted value '"
				+ valueBuf.toString() + "'");
	}

	private static class PropertyMap extends LinkedHashMap<String, String> {
		private final Map<String, String> originalKeys = new HashMap<String, String>();
		private static final String PROVIDER = normalize("Provider");

		public String get(Object key) {
			return super.get(normalize((String) key));
		}

		public String remove(Object key) {
			return super.remove(normalize((String) key));
		}

		public String put(String key, String value) {
			final String normalizedKey = normalize(key);
			if (normalizedKey.equals(PROVIDER) && containsKey(normalizedKey)) {
				// "Provider" is the sole property which does not override.
				// The first occurrence of "Provider" is the one which is used.
				return null;
			}
			originalKeys.put(normalizedKey, key);
			return super.put(normalizedKey, value);
		}

		public boolean containsKey(Object key) {
			return super.containsKey(normalize((String) key));
		}

		private static String normalize(String key) {
			return key.toUpperCase();
		}

		public String toString() {
			StringBuilder sb = new StringBuilder(64);
			int i = 0;
			for (Map.Entry<String, String> entry : entrySet()) {
				if (i++ > 0) {
					sb.append("; ");
				}
				final String key = entry.getKey();
				final String originalKey = originalKeys.get(key);
				sb.append(originalKey);
				sb.append('=');

				final String value = entry.getValue();
				if (value == null) {
					sb.append("'null'");
				} else {
					// Quote a property value if is has a semi colon in it
					// 'xxx;yyy'
					if (value.indexOf(';') >= 0 && value.charAt(0) != '\'') {
						sb.append("'");
					}

					sb.append(value);

					if (value.indexOf(';') >= 0 && value.charAt(value.length() - 1) != '\'') {
						sb.append("'");
					}
				}
			}
			return sb.toString();
		}
	}
}

// End ConnectStringParser.java
